-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(devvit): add new and improved version of mediareliability devvit…
… app
- Loading branch information
1 parent
eaeeb8c
commit 37416f2
Showing
15 changed files
with
1,344 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
name: test-app-4 | ||
version: 0.0.1.3 | ||
name: mediareliability | ||
version: 0.0.5.27 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import type { SetPostFlairOptions, TriggerContext } from '@devvit/public-api'; | ||
import { getTierDetails } from './helpers.js'; | ||
import type { AppSettings, PostData, Source } from './types.js'; | ||
|
||
/** | ||
* Matched sources are sorted by tier. | ||
* Don't flair the post if: | ||
* - The first source is official or an aggregator. | ||
* - The post is a self-post. | ||
*/ | ||
function shouldFlairPost(postData: PostData, sources: Source[]) { | ||
if (postData.url?.hostname && ['reddit.com', 'www.reddit.com'].includes(postData.url.hostname)) { | ||
return false; | ||
} | ||
|
||
const { postFlairText } = getTierDetails(sources[0].tier); | ||
|
||
return postFlairText !== null; | ||
} | ||
|
||
type FlairPostProps = { | ||
postId: string; | ||
sources: Source[]; | ||
subredditName: Exclude<SetPostFlairOptions['subredditName'], undefined>; | ||
flairCssClass: Exclude<SetPostFlairOptions['cssClass'], undefined>; | ||
flairTemplateId: Exclude<SetPostFlairOptions['flairTemplateId'], undefined>; | ||
context: TriggerContext; | ||
}; | ||
|
||
export async function flairPost({ postId, sources, subredditName, flairCssClass, flairTemplateId, context }: FlairPostProps) { | ||
const { postFlairText } = getTierDetails(sources[0].tier); | ||
|
||
if (!postFlairText) { | ||
return; | ||
} | ||
|
||
await context.reddit.setPostFlair({ | ||
postId, | ||
text: postFlairText, | ||
cssClass: flairCssClass, | ||
flairTemplateId: flairTemplateId, | ||
subredditName: subredditName | ||
}); | ||
} | ||
|
||
function getSourceLine(source: Source) { | ||
const { name, handles, tier, domains } = source; | ||
const { commentText, reliabilityText } = getTierDetails(tier); | ||
|
||
const handle = handles?.at(0) ?? null; | ||
const domain = domains?.at(0) ?? null; | ||
|
||
const sourceName = handle | ||
? `${name} ([@${handle}](https://twitter.com/${handle}))` | ||
: domain | ||
? `${name} ([${domain}](https://${domain}))` | ||
: name; | ||
|
||
return `**${commentText}**: ${sourceName}${reliabilityText ? ` - ${reliabilityText}` : ''}`; | ||
} | ||
|
||
type SubmitCommentProps = { | ||
postData: PostData; | ||
sources: Source[]; | ||
settings: AppSettings; | ||
context: TriggerContext; | ||
}; | ||
|
||
export async function submitComment({ postData, sources, settings, context }: SubmitCommentProps) { | ||
if (shouldFlairPost(postData, sources)) { | ||
await flairPost({ | ||
postId: postData.id, | ||
sources: sources, | ||
subredditName: postData.subredditName, | ||
flairCssClass: settings.flairCssClass, | ||
flairTemplateId: settings.flairTemplateId, | ||
context: context | ||
}); | ||
} | ||
|
||
const header = `**Media reliability report:**`; | ||
const warningForUnreliable = sources.some(source => getTierDetails(source.tier).unreliable) | ||
? settings.unreliableSourcesWarning | ||
: null; | ||
const footer = settings.commentFooter; | ||
|
||
const markdown = [ | ||
header, | ||
...sources.map(source => `- ${getSourceLine(source)}`), | ||
warningForUnreliable, | ||
footer | ||
] | ||
.filter(Boolean) | ||
.join('\n\n'); | ||
|
||
const comment = await context.reddit.submitComment({ | ||
id: postData.id, | ||
text: markdown | ||
}); | ||
|
||
await Promise.all([ | ||
comment.distinguish(true), | ||
comment.lock() | ||
]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import type { Context, TriggerContext } from '@devvit/public-api'; | ||
import linkifyit from 'linkify-it'; | ||
import { fromZodError } from 'zod-validation-error'; | ||
import { settingsSchema } from './schema.js'; | ||
import type { AppSettings, RedditPostV1, Source, TierDetails } from './types.js'; | ||
|
||
const linkify = new linkifyit(); | ||
|
||
/** | ||
* Remove diacritics and convert to lowercase | ||
* | ||
* @note There is a slight performance penalty when using modern unicode | ||
* property escapes so prefer using the old method with character class | ||
* range for now. | ||
* | ||
* @see https://stackoverflow.com/a/37511463/3258251 | ||
*/ | ||
export function normalizeText(text: string) { | ||
return text | ||
.normalize('NFD') | ||
.replace(/[\u0300-\u036f]/g, '') | ||
.toLowerCase(); | ||
} | ||
|
||
export function capitalizeString(string: string) { | ||
return string.charAt(0).toUpperCase() + string.slice(1); | ||
} | ||
|
||
/** | ||
* Details: | ||
* | ||
* order: used for sorting tiers | ||
* commentText: text to display in the comment | ||
* postFlairText: text to display in the post flair | ||
* reliabilityText: text to display in the comment after the source name | ||
* unreliable: whether the source is unreliable | ||
*/ | ||
const tierData: Record<Source['tier'], TierDetails> = { | ||
official: { | ||
order: 0, | ||
commentText: 'Official', | ||
postFlairText: 'Official', | ||
reliabilityText: 'official source', | ||
unreliable: false | ||
}, | ||
1: { | ||
order: 1, | ||
commentText: 'Tier 1', | ||
postFlairText: 'Tier 1', | ||
reliabilityText: 'very reliable', | ||
unreliable: false | ||
}, | ||
2: { | ||
order: 2, | ||
commentText: 'Tier 2', | ||
postFlairText: 'Tier 2', | ||
reliabilityText: 'reliable', | ||
unreliable: false | ||
}, | ||
3: { | ||
order: 3, | ||
commentText: 'Tier 3', | ||
postFlairText: 'Tier 3', | ||
reliabilityText: '❗ unreliable', | ||
unreliable: true | ||
}, | ||
4: { | ||
order: 4, | ||
commentText: 'Tier 4', | ||
postFlairText: 'Tier 4', | ||
reliabilityText: '❗ very unreliable', | ||
unreliable: true | ||
}, | ||
5: { | ||
order: 5, | ||
commentText: 'Tier 5', | ||
postFlairText: 'Tier 5', | ||
reliabilityText: '❗ extremely unrialable', | ||
unreliable: true | ||
}, | ||
aggregator: { | ||
order: 6, | ||
commentText: 'Aggregator', | ||
postFlairText: null, | ||
reliabilityText: null, | ||
unreliable: false | ||
}, | ||
}; | ||
|
||
export function getTierDetails(tier: Source['tier']) { | ||
return tierData[tier]; | ||
} | ||
|
||
export function sortTiers(a: Source, b: Source) { | ||
return getTierDetails(a.tier).order - getTierDetails(b.tier).order; | ||
} | ||
|
||
/** | ||
* Devvit onValidate is a bit weird, if you return string it assumes an error, | ||
* if you return undefined it assumes success, so here we return accordingly. | ||
*/ | ||
export function validateSetting(key: keyof AppSettings, value: unknown) { | ||
const parsed = settingsSchema.shape[key].safeParse(value); | ||
|
||
return parsed.success | ||
? undefined | ||
: `Invalid value for "${key}" setting. Error:\n ${fromZodError(parsed.error)}`; | ||
} | ||
|
||
export async function getAllSettings(context: Context | TriggerContext) { | ||
return settingsSchema.parse(await context.settings.getAll<AppSettings>()); | ||
} | ||
|
||
export function isIgnoredUser(username: string, settings: AppSettings) { | ||
return settings.ignoredUsers | ||
.some(ignoredUser => ignoredUser.toLowerCase() === username.toLowerCase()); | ||
} | ||
|
||
/** | ||
* This can be removed when the devvit remote tsc compiler is updated to ts 5.5+ | ||
* @see https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates | ||
*/ | ||
function nonNullable<T>(value: T): value is NonNullable<T> { | ||
return value !== null; | ||
} | ||
|
||
/** | ||
* Return all links in the post body, ignoring ones from reddit | ||
*/ | ||
function getPostLinks(body: string) { | ||
const links = (linkify.match(body) ?? []) | ||
.map(link => { | ||
try { | ||
const url = new URL(link.url, 'https://www.reddit.com'); | ||
return ['v.redd.it', 'i.redd.it', 'reddit.com', 'www.reddit.com'].includes(url.hostname) ? null : url; | ||
} | ||
catch (error) { | ||
return null; | ||
} | ||
}) | ||
.filter(nonNullable); | ||
|
||
return links.length > 0 ? links : null; | ||
} | ||
|
||
export function processPost(post: RedditPostV1) { | ||
const url = new URL(post.url, 'https://www.reddit.com'); | ||
const links = post.body ? getPostLinks(post.body) : null; | ||
|
||
return ({ | ||
id: post.id, | ||
subredditName: post.subredditName, | ||
titleNormalized: normalizeText(post.title), | ||
bodyNormalized: post.body && post.body.length > 0 ? normalizeText(post.body) : null, | ||
url: !['v.redd.it', 'i.redd.it', 'reddit.com', 'www.reddit.com'].includes(url.hostname) ? url : null, | ||
links: links, | ||
}); | ||
} | ||
|
||
export async function trySendPostErrorModmail(context: TriggerContext, postId: string, error: Error) { | ||
const { errorReportSubredditName } = await getAllSettings(context); | ||
|
||
if (errorReportSubredditName) { | ||
await context.reddit.sendPrivateMessage({ | ||
subject: 'An error occurred with the media reliability app', | ||
text: `An error occurred with this post: https://redd.it/${postId.replace(/^t3_/, '')}\n\n${String(error)}`, | ||
to: errorReportSubredditName | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './comment.js'; | ||
export * from './helpers.js'; | ||
export * from './matcher.js'; | ||
export * from './schema.js'; |
Oops, something went wrong.