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!
RunYetibot should reply with Hello World!
.
To see a more dynamic example, run uptime
:
!uptime
RunThe help
command lets you explore the commands available and their usage. It has its own help doc:
!help help
Run!help
RunYetibot 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
RunTo view the raw underlying Clojure data structure type of a response in Yetibot, use raw
. For example:
!list 1 2 3 | raw
RunPipes allow chaining together more complex expressions using the output of one command as the input of another command.
!echo 1 | echo 2 | echo 3
RunNotice 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
RunSubexpressions let you build up more complex expressions by nesting one expression in another.
!echo `echo Hello` Yetibot
RunBackticks 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`
RunThese 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
RunBelow we document a few highlights.
Meme gen was one of Yetibot's first features 😂.
!meme yetibot: hello world!
Run!help meme
Run!catfact | meme
Run!chuck | meme chuck:
Run!jargon | meme yeah if you:
Run!help giphy
Run!giphy reaction
Run!giphy whoa
Run!giphy random
Run!giphy trending
Run!help no
Run!no
Run!http 404
Run!http 200
Run!http 401
Run!http 500
Run!http 420
RunSince Yetibot can return a collection as a response, it needs basic operations to manipulate collections.
!help range
Run!range 10
Run!range 0 100 25
Run!help list
Run!list Yetibot is alive
Runxargs
allows us to operate over a collection, evaluating an expression for each value, in parallel.
!help xargs
Run!range 10 | xargs echo number | unwords
Run!help random
RunWith no args, random just produces a random number:
!random
RunBut if you pass it a collection it takes a random item from it:
!range 10 | random
Run!help head
Run!range 10 | head
Run!range 10 | head 3
Run!help tail
Run!range 10 | tail 3
Run!range 10 | head 3 | tail
Run!help shuffle
Run!range 10 | shuffle
Run!help words
Run!echo Delta compression using up to 8 threads | words
Run!help unwords
Run!range 10 | unwords
Run!echo Delta compression using up to 8 threads | words | shuffle | unwords
Run!help join
Run!range 10 | join
Run!range 10 | join 💥
Run!help split
Run!echo stick-in-the-mud | split -
RunThis is just like words
, except it splits apart letters.
!help letters
Run!echo Yetibot | letters
Run!help unletters
Run!echo Yetibot | letters | shuffle | unletters
Run!help trim
Run!echo why such space | trim
Run!help set
Run!list 1 1 2 2 3 3 | set
Run!help count
Run!range 10 | count
Run!help sum
Run!range 5 | sum
RunIt doesn't like it if you try to sum non-numbers:
!list one two three | sum
Run!help sort
Run!list foo bar baz | sort
Run!help sortnum
Run!list 22 foos, 33 bars, 11 bazes | sortnum
Run!help grep
Run!range 50 | grep 5
Run!range 50 | grep -C 1 5
Run!help tee
Runtee
doesn't work in the GraphQL API yet, but you can try this out via chat:
!echo foo | tee | echo bar
Run!help reverse
Run!range 3 | reverse
Run!help droplast
Run!list first middle last | droplast
Run!help rest
Run!range 10 | rest
RunThere 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
RunAliases 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
RunAs an example, look at the default output of stock
:
!stock tsla
RunLet's say we wanted a more concise one-liner using its data
:
!stock tsla | data show
Run!alias quickstock = 'stock $s | render {% ifequal change-percent|first "-" %}📉{% else %}📈{% endifequal %} {{symbol}} ${{price}} {{change-percent}} / ${{change}}'
RunLet's get multiple stocks at once:
!list aapl tsla vxus | xargs quickstock
RunAn observer listens for patterns and automatically runs commands when triggered. They're super powerful but can easily be abused.
!help observer
RunRun 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:
RunWith 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}}
RunThe 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}}
RunThis 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}}!
RunThe leave
event fires when a user leaves a channel.
!obs -c dev-testing -e leave = meme crying jordan: / bye {{username}}
RunYetibot 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
Run!category
Run!category list fun
Run!category list util
Run!category list repl
RunEach 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
RunDisable 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
RunFor example, we can get all the data behind the weather
command:
!weather seattle, wa | data show
RunThe 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}}
RunThe 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
Run!meme search computer gandalf | render ID of {{name}} at {{url}} is {{id}}
RunWe 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
RunThe 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
Run!help scrape
RunArbitrary 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
RunThe that
command retrieves the last non-command thing said, excluding Yetibot output, and that cmd
retrieves the last command.
!help that
RunExample usage:
!that | meme insanity:
The karma
command lets you increment or decrement another user's karma along with an optional note.
!help karma
RunView the leaderboard:
!karma
RunThere 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
Run!karma @yetibot-- you had one job
RunThe 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
RunIn 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
RunList the open PRs for Yetibot:
!gh pr yetibot
Runor try Clojure:
!gh pr clojure
RunList Yetibot's repos:
!gh repos yetibot
RunList the tags on yetibot.core
:
!gh tags yetibot/yetibot.core
RunList contributors on the yetibot/yetibot
repo since last month:
!gh contributors yetibot/yetibot since 1 month ago
RunLook up git change stats on Clojure:
!gh stats clojure/clojure
RunLook up git change stats on Yetibot's first 3 repos:
!gh repos yetibot | head 3
Run!gh repos yetibot | head 3 | xargs words | xargs head | xargs gh stats
RunYetibot'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
RunLet's start by creating a new issue:
!jira create new issue from yetibot.com!
RunIf we list recent issues and you should see the issue you just created at the top:
!jira recent
RunWe can get more details on that issue:
!jira recent | head | jira parse | jira show
RunLet'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
RunWe could also parse issue keys out of all recent issues via:
!jira recent | xargs jira parse
RunWhat 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
RunCreate a sub-task of our most recent issue:
!jira recent | head | jira parse | jira create I am a subtask -p %s
RunIf you want to quickly search for issues:
!jira search demo
RunThis 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"
RunCheckout 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
RunWe can also assign issues, but first let's see who's all available:
!jira project-users
RunNote: we can also search for users, system wide:
!jira users t
Run!jira users t | data show
RunAssign 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`
RunOr assign all issues that mention docs
in the summary to Yetibot:
!jira jql summary ~ docs | xargs jira parse | xargs jira assign %s Yetibot
RunLet's add a worklog item:
!jira recent | random | jira parse | jira worklog %s 15m explored the docs
RunList components:
!jira components
RunAll of the JIRA commands expose data. For example, take a peek at the full API response for a component:
!jira components | head | data show
RunThis 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}}
RunWe can also list things like versions, issue types, and projects:
!jira versions
Run!jira issue-types
Run!jira projects
RunSearch for projects matching yet
:
!jira projects yet
RunPeek at some raw data behind the pipe:
!jira projects | random | data show
RunWe 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
Run!jira jql summary ~ thing AND resolution = Unresolved
Run!jira jql summary ~ thing AND resolution = Unresolved | random | jira parse | jira comment %s I `list agree,disagree | random`
RunBatch resolve all the thing
s:
!jira jql summary ~ thing AND resolution = Unresolved | xargs render {{key}}
Run!jira jql summary ~ thing AND resolution = Unresolved | xargs render {{key}} | xargs jira resolve %s Fixed
RunNow clean your JIRA with jira delete
:
!jira jql summary ~ thing AND resolution = Done | xargs jira parse | xargs jira delete
Run🔥
And if you want to really clean up, delete all issues created today:
!jira jql created >= startOfDay()
Run!jira jql created >= startOfDay() | xargs jira parse | xargs jira delete
Run🔥😑
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
Run!ftoc 100
RunThis 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
RunWe 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
RunHint: 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 🧐