User Guide
Interactive guide to using your Yetibot
Interactive guide to using your Yetibot
All the examples below are interactive. Click the button below each example evaluate the expression against the live, public Yetibot instance using the GraphQL API.
This is what a simple command looks like:
!echo Hello World!
Yetibot should reply with Hello World!
.
To see a more dynamic example, run uptime
:
!uptime
The help
command lets you explore the commands available and their usage. It has its own help doc:
!help help
!help
Yetibot responses are either:
There are no other types in Yetibot, though the underlying Clojure data structures in responses may contain more diverse responses, such as when returning the result of an API call.
Here's an example of a collection:
!list 1 2 3
To view the raw underlying Clojure data structure type of a response in Yetibot, use raw
. For example:
!list 1 2 3 | raw
Pipes allow chaining together more complex expressions using the output of one command as the input of another command.
!echo 1 | echo 2 | echo 3
Notice how the output of each preceding command is appended to the end of the following command separated by a space. This is the default behavior, but you can also choose where to place arguments and control whitespace using %s
:
!echo foo | echo %sbar
Subexpressions let you build up more complex expressions by nesting one expression in another.
!echo `echo Hello` Yetibot
Backticks are convenient when you need a single level of nesting, but $()
syntax lets you embed any level of nesting:
!echo $(echo $(echo Yetibot) is) `echo alive`
These examples are necessarily simplistic but when you start piecing together more complex commands that fetch data, the ability to arbitrarily nest expressions is quite useful.
Fun has always been an important part of Yetibot's existence. The full list of commands in the fun
category can be listed via:
!category list fun
Below we document a few highlights.
Meme gen was one of Yetibot's first features 😂.
!meme yetibot: hello world!
!help meme
!catfact | meme
!chuck | meme chuck:
!jargon | meme yeah if you:
!help giphy
!giphy reaction
!giphy whoa
!giphy random
!giphy trending
!help no
!no
!http 404
!http 200
!http 401
!http 500
!http 420
Since Yetibot can return a collection as a response, it needs basic operations to manipulate collections.
!help range
!range 10
!range 0 100 25
!help list
!list Yetibot is alive
xargs
allows us to operate over a collection, evaluating an expression for each value, in parallel.
!help xargs
!range 10 | xargs echo number | unwords
!help random
With no args, random just produces a random number:
!random
But if you pass it a collection it takes a random item from it:
!range 10 | random
!help head
!range 10 | head
!range 10 | head 3
!help tail
!range 10 | tail 3
!range 10 | head 3 | tail
!help shuffle
!range 10 | shuffle
!help words
!echo Delta compression using up to 8 threads | words
!help unwords
!range 10 | unwords
!echo Delta compression using up to 8 threads | words | shuffle | unwords
!help join
!range 10 | join
!range 10 | join 💥
!help split
!echo stick-in-the-mud | split -
This is just like words
, except it splits apart letters.
!help letters
!echo Yetibot | letters
!help unletters
!echo Yetibot | letters | shuffle | unletters
!help trim
!echo why such space | trim
!help set
!list 1 1 2 2 3 3 | set
!help count
!range 10 | count
!help sum
!range 5 | sum
It doesn't like it if you try to sum non-numbers:
!list one two three | sum
!help sort
!list foo bar baz | sort
!help sortnum
!list 22 foos, 33 bars, 11 bazes | sortnum
!help grep
!range 50 | grep 5
!range 50 | grep -C 1 5
!help tee
tee
doesn't work in the GraphQL API yet, but you can try this out via chat:
!echo foo | tee | echo bar
!help reverse
!range 3 | reverse
!help droplast
!list first middle last | droplast
!help rest
!range 10 | rest
There are a few others not documented here such as raw
, keys
, vals`, and data
(which is documented in its own section of this guide). To view them all we can look them up by category:
!category list collection
Aliases are similar to alias
in bash allowing us to give a name for a command. This is typically a heavily-used feature as teams build up aliases for fun or utility and become a manifestation of culture.
!help alias
As an example, look at the default output of stock
:
!stock tsla
Let's say we wanted a more concise one-liner using its data
:
!stock tsla | data show
!alias quickstock = 'stock $s | render {% ifequal change-percent|first "-" %}📉{% else %}📈{% endifequal %} {{symbol}} ${{price}} {{change-percent}} / ${{change}}'
Let's get multiple stocks at once:
!list aapl tsla vxus | xargs quickstock
An observer listens for patterns and automatically runs commands when triggered. They're super powerful but can easily be abused.
!help observer
Run the above help docs. As you can see, observers support several different event types.
The default event type for observers is message
. This allows Yetibot to react to a message by running a command. For example:
!obs sushi = react :sushi:
With this observer any time anyone mention sushi they get a 🍣 reaction. Note that we could have specified a regex pattern instead of the literal "sushi".
It also supports optional settings that let us filter on:
-c
-u
For example:
!obs -c dev-testing -u devth = echo {{username}} said {{body}} in {{channel}}
The above example also illustrates the templating functionality. Along with username
and body
, channel
is available on all event types, and for react
observer events, reaction
and message-username
are also available.
A react
event fires when a user reacts to a message (Slack only).
!obs -c dev-testing -e react = giphy {{reaction}} {{body}}
This causes a giphy lookup using the reaction used (e.g. 100
or smile
) and the body text of the message that was reacted to.
React events also have two other fields available: reaction
and message-username
where message-username
is the username of the user that posted the original message (username
is the username
of the user that reacted).
The enter
event fires when a user enters a channel.
!obs -c dev-testing -e enter = meme internet: welcome to {{channe}} {{username}}!
The leave
event fires when a user leaves a channel.
!obs -c dev-testing -e leave = meme crying jordan: / bye {{username}}
Yetibot commands can be organized under categories. Commands can be enabled or disabled according to channel settings (e.g. :fun
). In the future, help
might rely on categories.
Categories are stored as meta-data directly on command handler functions under a :yb/cat
prefix with a Set
of keywords as the value.
Current known categories are as follows. Please add to this list as needed. Some categories will overlap but are semantically distinct.
!help category
!category
!category list fun
!category list util
!category list repl
Each category can be disabled or enabled at the channel level. By default all categories are enabled. To disable them, use:
!category disable :category-name
Show the list of categories and their docs:
!category
Disable the "fun" category:
!category disable fun
Re-enable it:
!category enable fun
Yetibot can ssh
into servers and run commands on your behalf.
The config looks like:
{:yetibot
{;; ...
:ssh {:groups
[{:user "",
:servers [{:host "", :name ""} {:host "", :name ""}],
:key "path-to-key"}]},
}}
Each map inside the :groups
collection has a single user
and key
and allows multiple servers.
For example, if we had a server with name example
we could see it in the list of servers via:
!ssh servers
and run commands via:
!ssh example echo foo
Yetibot ssh
currently only supports key-based authentication.
Yetibot commands by default return a pretty-printed response for human consumption, but for many commands the underlying data is preserved and passed across the pipe as well:
!help data
For example, we can get all the data behind the weather
command:
!weather seattle, wa | data show
The render
command lets us turn the data discussed in the previous section into strings, which means we can customize the output of a command using the data behind it!
Using our weather
example again:
!weather seattle | render Wind in {{city_name}} at {{wind_spd|multiply:2.23694|double-format:2}} mph blowing {{wind_cdir_full|capitalize}}
The templating support is provided by Selmer. It supports a number of filters, e.g. {{name|capitalize}}
. See the Selmer docs for more!
Need to know the ID of the computer gandalf
meme for some reason?
!meme search computer gandalf | data show
!meme search computer gandalf | render ID of {{name}} at {{url}} is {{id}}
We could go on and on (and on and on). render
opens up a ton of possabilities for customizing command output and using Yetibot in unexpected and unanticipated ways!
Cron is, as you'd expect, a way to run a command on an interval given a cron expression.
!help cron
The eval
command runs arbitrary Clojure against the Yetibot process itself, so it's by definition very insecure. Because of this, it's only allowed to be run by users that have been pre-configured to have access.
It can be a fun way of poking at otherwise-unavailable state inside Yetibot.
!help eval
!help scrape
Arbitrary key/value pairs can be stored on a per-channel basis. This lets you do things like set channel local JIRA projects, Jenkins jobs, or other values that could for example be utilized by aliases.
!help channel
The that
command retrieves the last non-command thing said, excluding Yetibot output, and that cmd
retrieves the last command.
!help that
Example usage:
!that | meme insanity:
The karma
command lets you increment or decrement another user's karma along with an optional note.
!help karma
View the leaderboard:
!karma
There are three ways to adjust a user's Karma.
The canonical !karma
command invocation, using the traditional C increment (++
) or decrement (–
) operators, optionally followed by a note.
!karma @yetibot++ thanks Yetibot
!karma @yetibot-- you had one job
The Emoji versions to add (🌈) or subtract (⛈), optionally followed by a note.
🌈 @yetibot
⛈ @yetibot I told you to go before we got in the car!
Via Emoji reactions, which necessarily preclude the addition of notes.
At this time removing your Emoji reaction does not reverse the initial addition or subtraction of Karma.
In all cases, Yetibot will respond with the user's new score. Emoji reaction Karma is reported in a thread.
Now take a look at Yetibot's Karma:
!karma @yetibot
In addition to these commands, karma
is also exposed via the GraphQL API!
Try a query like:
# scores
curl -s 'https://public.yetibot.com/graphql' \
-H 'Accept: application/json' \
--data 'query={karmas(report: SCORES) {user_id score}}' \
--compressed | jq
# givers
curl -s 'https://public.yetibot.com/graphql' \
-H 'Accept: application/json' \
--data 'query={karmas(report: GIVERS) {user_id score}}' \
--compressed | jq
# inspect Yetibot's karma
curl -s 'https://public.yetibot.com/graphql' \
-H 'Accept: application/json' \
--data 'query={user(id:"U3K6P707R"){username,karma}}' \
--compressed | jq
You can configure the positive and negative Emojis used in commands and reactions, overriding the default 🌈 and ⛈. Have a look at config/config.sample.edn.
Please use Slack shortcodes and not the Unicode characters themselves.
:karma {:emoji {:positive ":taco:" :negative ":poop:"}}}}
!help gh
List the open PRs for Yetibot:
!gh pr yetibot
or try Clojure:
!gh pr clojure
List Yetibot's repos:
!gh repos yetibot
List the tags on yetibot.core
:
!gh tags yetibot/yetibot.core
List contributors on the yetibot/yetibot
repo since last month:
!gh contributors yetibot/yetibot since 1 month ago
Look up git change stats on Clojure:
!gh stats clojure/clojure
Look up git change stats on Yetibot's first 3 repos:
!gh repos yetibot | head 3
!gh repos yetibot | head 3 | xargs words | xargs head | xargs gh stats
Yetibot's JIRA capabilities are pretty powerful, especially when combined with Yetibot's expressions pipeline capabilities. It can operate over a global list of projects or per-channel project settings. Operations include creating, updating, resolving, describing, deleting, assigning, logging work, and commenting on issues.
It can also search issues using JIRA's powerful jql
syntax.
!help jira
Let's start by creating a new issue:
!jira create new issue from yetibot.com!
If we list recent issues and you should see the issue you just created at the top:
!jira recent
We can get more details on that issue:
!jira recent | head | jira parse | jira show
Let's break this down keeping in mind values flow across |
pipes:
jira recent
- list the recent issueshead
- take the first issue from the listjira parse
- extract the issue key from a url, e.g. extract YETIBOT-X
from https://yetibot.atlassian.net/browse/YETIBOT-X
jira show
- describe the issueIf we knew the issue key ahead of time, we could have simply used it:
!jira show YETIBOT-4
We could also parse issue keys out of all recent issues via:
!jira recent | xargs jira parse
What if you want to create a subtask? We can do that too using the -p
option to specify the parent when creating:
!help jira | grep create
Create a sub-task of our most recent issue:
!jira recent | head | jira parse | jira create I am a subtask -p %s
If you want to quickly search for issues:
!jira search demo
This runs a JQL query that looks like:
project in (YETIBOT) AND
(summary ~ "demo" OR description ~ "demo" OR comment ~ "demo")
It searches across 3 fields:
summary
description
comment
But you can also drop down to raw JQL when the need arises:
!jira jql created >= "-5h"
Checkout Atlassian's Advanced searching docs and Advanced searching - operators reference for more info on using JQL. It's super powerful.
Let's look at some of the ways we can interact with our issue with the power of Yetibot expressions.
!jira recent | head | jira parse | jira comment %s Works on my machine
We can also assign issues, but first let's see who's all available:
!jira project-users
Note: we can also search for users, system wide:
!jira users t
!jira users t | data show
Assign a random issue to a random user (try running this multiple times for extra chaos 😈):
!jira recent | random | jira parse | jira assign %s `jira project-users | random`
Or assign all issues that mention docs
in the summary to Yetibot:
!jira jql summary ~ docs | xargs jira parse | xargs jira assign %s Yetibot
Let's add a worklog item:
!jira recent | random | jira parse | jira worklog %s 15m explored the docs
List components:
!jira components
All of the JIRA commands expose data. For example, take a peek at the full API response for a component:
!jira components | head | data show
This gives us flexability to get at any piece of the data that might not be included in Yetibot's default formatted output. What if we wanted to get the avatar of the first component's lead?
!jira components | head | render {{lead.avatarUrls.48x48}}
We can also list things like versions, issue types, and projects:
!jira versions
!jira issue-types
!jira projects
Search for projects matching yet
:
!jira projects yet
Peek at some raw data behind the pipe:
!jira projects | random | data show
We can combine any of these with Yetibot's pipeline capabilities and do batch operations.
!list do the right thing, do the best thing, do the fastest thing | xargs jira create
!jira jql summary ~ thing AND resolution = Unresolved
!jira jql summary ~ thing AND resolution = Unresolved | random | jira parse | jira comment %s I `list agree,disagree | random`
Batch resolve all the thing
s:
!jira jql summary ~ thing AND resolution = Unresolved | xargs render {{key}}
!jira jql summary ~ thing AND resolution = Unresolved | xargs render {{key}} | xargs jira resolve %s Fixed
Now clean your JIRA with jira delete
:
!jira jql summary ~ thing AND resolution = Done | xargs jira parse | xargs jira delete
🔥
And if you want to really clean up, delete all issues created today:
!jira jql created >= startOfDay()
!jira jql created >= startOfDay() | xargs jira parse | xargs jira delete
🔥😑
JIRA configuration looks like:
:jira {:default {:project {:key "Optional"}, :issue {:type {:id "3"}}},
:user "",
:projects [{:default {:version {:id "42"}}, :key "FOO"}],
:subtask {:issue {:type {:id "27"}}},
:domain "",
:password ""},
The projects
key is optional and specifies the global list of projects. This is mostly important for the observer that detects a JIRA task like PROJ-123
and auto outputs the details of the task, when found.
This ability is also configurable on a per-channel basis using channel settings:
!channel set jira-project YETIBOT,COM
After setting that in a channel, Yetibot will know to expand all occurrences of strings that look like JIRA keys, such as YETIBOT-123
or COM-2
.
Yetibot serves a GraphQL endpoint as part of its web app. The public Yetibot instance is accessible at https://public.yetibot.com/graphql.
For example, to inspect the configured adapters, run the following query with a GraphQL client:
{
adapters {
uuid
platform
}
}
Or if you prefer examples in curl
:
# list the configured Adapters
curl -s 'https://public.yetibot.com/graphql' \
-H 'Accept: application/json' \
--data 'query={adapters {uuid platform}}' \
--compressed | jq
# get some stats about this Yetibot
curl -s 'https://public.yetibot.com/graphql' \
-H 'Accept: application/json' \
--data 'query={stats {uptime adapter_count user_count command_count}}' \
--compressed | jq
These recipes are fun example usages of Yetibot that more holistically communicate its capabilities and historical usage.
The built in render
command is quite powerful because it uses the Selmer library for rendering templates, which includes many filters for postprocessing a value.
The following example illustrates this by implementing Celcius to Fahrenheit conversion, and vice versa, in terms of render
:
!alias ctof = "echo $s°C = `render {{$s|multiply:1.8|add:32|round}}°F`"
!alias ftoc = "echo $s°F = `render {{$s|add:-32|multiply:5|divide:9|round}}°C`"
!ctof 0
!ftoc 100
This alias uses JavaScript to determine the number of days till a specific date and outputs the day along with a string message. It could be further abstracted as a countdown
alias that takes a date and a message, but we typically just re-define the alias as needed.
!alias countdown = "js Math.ceil((Date.parse('4/2/2019') - Date.now()) / (1000 * 60 * 60 * 24)) + ' days until Google+ shutdown'"
!countdown
We use an alias to store the list of zip codes for all members of our distributed team, which we then feed to another alias which outputs a short single-line description of current weather for a location.
!alias locs = list 59101 98101 94101
!alias loctemp = "weather $s | xargs sed s/Current conditions for / | head 2 | reverse | unwords"
!alias alltemps = "locs | xargs loctemp | sort"
!alltemps
25.4 F (-3.7 C), Mostly Cloudy Jupiter Sulphur, Billings, Montana elevation 3114 ft:
43.2 F (6.2 C), Partly Cloudy Belltown, Seattle, Washington elevation 135 ft:
49.8 F (9.9 C), Overcast SOMA - Near Van Ness, San Francisco, California elevation 49 ft:
I'm pretty fond of this one. I needed really wanted a way to seed a meme alias with valid sentences like:
[user] should not [do something]
Time for some Google Complete, JavaScript, and a few aliases!
First, we need a random letter to seed Google Complete with, for ultimate variety of "shoulds":
!alias randletter = "range 65 91 | xargs echo | random | js String.fromCharCode(%s)"
!repeat 5 randletter | unwords
V L R U S
LGTM! ☝️
Now pipe that to a well-crafted complete
query:
!randletter | complete should i | random | sed s/should i/
invest in bonds
Awesome, let's alias it:
!alias randshould = "randletter | complete should i | random | sed s/should i/"
!repeat 10 randshould
take creatine
call her
max out my 401k
move to san francisco
shave my beard
have kids
buy a house
do it
feed feral cats
have a savings account
Finally:
!alias usershouldnot = "user | random | echo %s should not `randshould`"
!usershouldnot | meme angry wolf:
Or try the variant, yetishould
:
!yetishould
Hint: run it more than once 😁
!alias dis = echo ಠ_ಠ
!alias grid = "repeat 3 echo $(repeat 3 echo $s | unwords)"
!grid `dis`
ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ
!grid `dis` | xargs grid
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
!grid `dis` | xargs grid | xargs xargs grid
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ ಠ_ಠ
I think you get the idea 🧐