Nimbi

Twitter.ts

Sep 10th, 2021 (edited)
287
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import { config } from '../../modules/config/cfg';
  2. import { permissions } from '../../modules/config/permissions';
  3.  
  4. const TwitterSubCfg = {
  5.   TWITTER_API_KEY: config.api.TWITTER_API,
  6.   TWITTER_BEARER_TOKEN: config.api.TWITTER_BEARER,
  7.   ICON_URL: config.images.twitterIcon
  8. };
  9.  
  10. const errMsg = config.modules.errors.response;
  11. const Decor = discord.decor;
  12.  
  13. class TwitterClient {
  14.   private static AuthHeader: RequestInit = {
  15.     headers: [['Authorization', 'Bearer ' + TwitterSubCfg.TWITTER_BEARER_TOKEN]]
  16.   };
  17.  
  18.   static async getTweets(
  19.     userId?: string,
  20.     sinceId?: string,
  21.     count?: number
  22.   ): Promise<Tweet[]> {
  23.     let url = new URL(
  24.       'https://api.twitter.com/1.1/statuses/user_timeline.json'
  25.     );
  26.  
  27.     if (userId != undefined) {
  28.       url.searchParams.append('user_id', userId);
  29.     }
  30.     if (sinceId != undefined) {
  31.       url.searchParams.append('since_id', sinceId);
  32.     }
  33.     if (count != undefined) {
  34.       url.searchParams.append('count', count.toString());
  35.     }
  36.  
  37.     let query = await fetch(url, TwitterClient.AuthHeader);
  38.     let json = await query.json();
  39.     let rawJson = JSON.stringify(json);
  40.     return JSON.parse(rawJson);
  41.   }
  42.  
  43.   static async getUserIdByUserName(username: string): Promise<string | null> {
  44.     let url = new URL('https://api.twitter.com/2/users/by/username/');
  45.     url.pathname += username;
  46.     let query = await fetch(url, TwitterClient.AuthHeader);
  47.     let json = await query.json();
  48.  
  49.     if (json['errors']?.length > 0) {
  50.       return null;
  51.     }
  52.  
  53.     return json['data']['id'];
  54.   }
  55.  
  56.   static async getUserNames(userIds: string[]): Promise<string[]> {
  57.     let url = new URL('https://api.twitter.com/2/users');
  58.     url.searchParams.append('ids', userIds.join(','));
  59.     let query = await fetch(url, TwitterClient.AuthHeader);
  60.     let json = await query.json();
  61.     return json['data'].map((x: any) => x.username);
  62.   }
  63. }
  64.  
  65. const twitterSubKv = new pylon.KVNamespace('twitter-subs');
  66.  
  67. interface TwitterSub {
  68.   channelId: string;
  69.   lastTweetId?: string;
  70. }
  71.  
  72. interface Tweet {
  73.   id_str: string;
  74.   user: TwitterUser;
  75. }
  76.  
  77. interface TwitterUser {
  78.   screen_name: string;
  79. }
  80.  
  81. config.commands.subcommand(
  82.   {
  83.     name: 'twitter',
  84.     description: 'Twitter feeds module',
  85.     filters: permissions.user
  86.   },
  87.   (subcmd) => {
  88.     subcmd.on(
  89.       {
  90.         name: 'subscribe',
  91.         description: 'Subscribe to a Twitter feed.',
  92.         aliases: ['sub', 'add', '+'],
  93.         filters: permissions.helper
  94.       },
  95.       (ctx) => ({
  96.         username: ctx.string(),
  97.         channel: ctx.stringOptional()
  98.       }),
  99.       async (msg, { username, channel }) => {
  100.         const Decor = discord.decor;
  101.         let userId = await TwitterClient.getUserIdByUserName(username);
  102.         if (userId == null) {
  103.           await msg.reply(
  104.             createEmbedMessage(
  105.               Decor.Emojis.X +
  106.                 ' Cannot find Twitter user with the name **' +
  107.                 username +
  108.                 '**'
  109.             )
  110.           );
  111.           return;
  112.         }
  113.  
  114.         let existingSub = await twitterSubKv.get<string>(userId);
  115.         if (existingSub != null) {
  116.           let sub: TwitterSub = JSON.parse(existingSub);
  117.           let subChannel = await discord.getGuildTextChannel(sub.channelId);
  118.           await msg.reply(
  119.             createEmbedMessage(
  120.               Decor.Emojis.X +
  121.                 ' There is a subscription already to this feed in ' +
  122.                 subChannel?.toMention() ?? '*Unknown channel*'
  123.             )
  124.           );
  125.           return;
  126.         }
  127.  
  128.         let channelId =
  129.           channel == null ? msg.channelId : stripMentionableChannelId(channel);
  130.         let ch = await discord.getGuildTextChannel(channelId);
  131.  
  132.         if (ch == null) {
  133.           msg.reply(createEmbedMessage(Decor.Emojis.X + errMsg.InvalidChannel));
  134.           return;
  135.         }
  136.  
  137.         let twitterSub: TwitterSub = {
  138.           channelId: stripMentionableChannelId(channelId)
  139.         };
  140.         await twitterSubKv.put(userId, JSON.stringify(twitterSub));
  141.  
  142.         let twitterNames = await TwitterClient.getUserNames([userId]);
  143.         await msg.reply(
  144.           createEmbedMessage(
  145.             ' Successfully subscribed to [@' +
  146.               twitterNames[0] +
  147.               '](https://twitter.com/${twitterNames[0]}) in ' +
  148.               ch.toMention() +
  149.               '!'
  150.           )
  151.         );
  152.  
  153.         await fetchTweets();
  154.       }
  155.     );
  156.  
  157.     subcmd.on(
  158.       {
  159.         name: 'unsubscribe',
  160.         aliases: ['unsub', 'remove', 'delete', 'del', 'rem', '-'],
  161.         description: 'Unsubscribe from a Twitter feed.',
  162.         filters: permissions.helper
  163.       },
  164.       (ctx) => ({ username: ctx.string() }),
  165.       async (msg, { username }) => {
  166.         let userId = await TwitterClient.getUserIdByUserName(username);
  167.         if (userId == null) {
  168.           await msg.reply(
  169.             createEmbedMessage(
  170.               Decor.Emojis.X +
  171.                 ' Cannot find Twitter user with the name **' +
  172.                 username +
  173.                 '**'
  174.             )
  175.           );
  176.           return;
  177.         }
  178.  
  179.         let sub = await twitterSubKv.get<string>(userId);
  180.         if (sub == null) {
  181.           await msg.reply(
  182.             createEmbedMessage(
  183.               Decor.Emojis.X + ' There is no subscription for this feed'
  184.             )
  185.           );
  186.           return;
  187.         }
  188.  
  189.         await twitterSubKv.delete(userId);
  190.  
  191.         let twitterNames = await TwitterClient.getUserNames([userId]);
  192.         await msg.reply(
  193.           createEmbedMessage(
  194.             Decor.Emojis.WHITE_CHECK_MARK +
  195.               ' Successfully unsubscribed from [@' +
  196.               twitterNames[0] +
  197.               '](https://twitter.com/' +
  198.               twitterNames[0] +
  199.               ')'
  200.           )
  201.         );
  202.       }
  203.     );
  204.  
  205.     subcmd.raw(
  206.       {
  207.         name: 'list',
  208.         aliases: ['ls', 'l', 'display'],
  209.         description: 'List all subscribed Twitter feeds.',
  210.         filters: permissions.user
  211.       },
  212.       async (msg) => {
  213.         let keys = await twitterSubKv.list();
  214.         if (keys.length == 0) {
  215.           await msg.reply(
  216.             createEmbedMessage(Decor.Emojis.X + ' There are no subscriptions')
  217.           );
  218.           return;
  219.         }
  220.  
  221.         let items = await twitterSubKv.items();
  222.         let usernames = await TwitterClient.getUserNames(keys);
  223.         let listMsg = new Array<string>();
  224.  
  225.         for (let i = 0; i < keys.length; i++) {
  226.           let sub: TwitterSub = JSON.parse(items[i].value as string);
  227.           let channel = await discord.getGuildTextChannel(sub.channelId);
  228.           listMsg.push(
  229.             '@' + usernames[i] + ' -> ' + channel?.toMention() ??
  230.               '*Unknown Channel*'
  231.           );
  232.         }
  233.  
  234.         await msg.reply(
  235.           createEmbedMessage(
  236.             listMsg.join('\n'),
  237.             'Subscriptions',
  238.             keys.length + ' subscribed feeds'
  239.           )
  240.         );
  241.       }
  242.     );
  243.  
  244.     subcmd.raw(
  245.       {
  246.         name: 'poll',
  247.         aliases: ['count', 'c', 'p'],
  248.         description: 'Returns the tweet count of a specified twitter user',
  249.         filters: permissions.user
  250.       },
  251.       async (msg) => {
  252.         let tweetCount = await fetchTweets();
  253.         await msg.reply(
  254.           createEmbedMessage(
  255.             Decor.Emojis.WHITE_CHECK_MARK +
  256.               ' Manual polling found ' +
  257.               tweetCount +
  258.               ' new tweets'
  259.           )
  260.         );
  261.       }
  262.     );
  263.   }
  264. );
  265.  
  266. pylon.tasks.cron('twitter-sub', '0 0/5 * * * * *', async () => {
  267.   await fetchTweets();
  268. });
  269.  
  270. async function fetchTweets(): Promise<number> {
  271.   let keys = await twitterSubKv.list();
  272.   if (keys.length == 0) {
  273.     return 0;
  274.   }
  275.  
  276.   let tweetCount = 0;
  277.   for (let key of keys) {
  278.     tweetCount += await fetchFeed(key);
  279.   }
  280.  
  281.   return tweetCount;
  282. }
  283.  
  284. async function fetchFeed(twitterUserId: string): Promise<number> {
  285.   let sub = await getSub(twitterUserId);
  286.   let tweets = await TwitterClient.getTweets(
  287.     twitterUserId,
  288.     sub.lastTweetId,
  289.     sub.lastTweetId == undefined ? 1 : undefined
  290.   );
  291.   if (tweets.length > 0) {
  292.     await twitterSubKv.transact<string>(twitterUserId, (prev) => {
  293.       let sub: TwitterSub = JSON.parse(prev as string);
  294.       sub.lastTweetId = tweets[0].id_str;
  295.       return JSON.stringify(sub);
  296.     });
  297.  
  298.     let sub = await getSub(twitterUserId);
  299.     await sendTweets(sub.channelId, tweets);
  300.   }
  301.  
  302.   return tweets.length;
  303. }
  304.  
  305. async function sendTweets(channelId: string, tweets: Tweet[]): Promise<void> {
  306.   let channel = await discord.getGuildTextChannel(channelId);
  307.   for (let tweet of tweets) {
  308.     await channel?.sendMessage({
  309.       content:
  310.         'http://twitter.com/' +
  311.         tweet.user.screen_name +
  312.         '/status/' +
  313.         tweet.id_str
  314.     });
  315.   }
  316. }
  317.  
  318. async function getSub(key: string): Promise<TwitterSub> {
  319.   let subJson = await twitterSubKv.get<string>(key);
  320.   return JSON.parse(subJson as string);
  321. }
  322.  
  323. function stripMentionableChannelId(mentionnableChannelId: string): string {
  324.   return mentionnableChannelId.replace('<#', '').replace('>', '');
  325. }
  326.  
  327. function createEmbedMessage(
  328.   description?: string,
  329.   title?: string,
  330.   footerText?: string
  331. ): discord.Message.OutgoingMessageArgument<
  332.   discord.Message.OutgoingMessageOptions
  333. > {
  334.   let embed = new discord.Embed({
  335.     author: {
  336.       name: 'Twitter Sub',
  337.       iconUrl: TwitterSubCfg.ICON_URL
  338.     },
  339.     title,
  340.     description
  341.   });
  342.  
  343.   if (footerText != undefined) {
  344.     embed.setFooter({
  345.       text: footerText
  346.     });
  347.   }
  348.  
  349.   return { embed };
  350. }
  351.  
Add Comment
Please, Sign In to add comment